# Import Python's XML parsing class.
from xml.dom import minidom

# Import some standard Python network libraries.
import urllib, urllib2

# Import the jinja2 templating engine for XML production.
from hsvo.jinja2 import Environment, FileSystemLoader

# Import some deployment-specific constants.
from hsvo.constants import OL_ROOT_URL, SAVOIR_ENDPOINT, SERVICE_ID, BI_DEBUG

class HSVODeviceInterface():
    """
    HSVODeviceInterface
    
    This class provides an edge device interface conforming to 
    actions as defined in the HSVO XML message specification.
    """
    
    def __init__(self):
        self.templateEnvironment = Environment(loader=FileSystemLoader('templates'))
        self.templateEnvironment.globals.update({'SAVOIR_ENDPOINT': SAVOIR_ENDPOINT, 'SERVICE_ID': SERVICE_ID})
    
    def authenticate(self, parameters):
        """
        authenticate(parameters) - This method is responsible for device 
        authentication and returns a string token (referenced elsewhere as sessionID). 
        If the authentication fails, the error is printed and the returned value is false.
        The parameters argument is formatted as follows:
        
        parameters = {
            parameter.id: {
                'id': parameter.id,
                'name': parameter.name,
                'unit': parameter.unit,
                'value': parameter.value
            },{
                ...
            }
        }
        
        Throws:
            -OperationFailed if the device connection fails.
            -OperationNotSupported if the device doesn't support the HSVO action type.
        """
        
        # Login to the device by submitting a username/password to its login service.
        url = OL_ROOT_URL + 'login/'
        username = parameters.get('username').get('value')
        password = parameters.get('password').get('value')
        postData = urllib.urlencode({'username': username, 'password': password})
        
        if BI_DEBUG:
            print 'Authenticating the device using username "%s" and password "%s".' % (username, password)
            print 'Post data: "%s".' % (postData,)
        
        # Submit the post data and read the results, which should 
        # be a new sessionID if the login was successful.
        try:
            usock = urllib2.urlopen(url, postData)
            sessionID = usock.read().strip()
            usock.close()
        
        except urllib2.HTTPError:
            raise OperationFailed('There was an HTTP error when trying to authenticate the session.')
        
        return sessionID
    
    def load(self, **parameters):
        """
        load(**parameters) - This method is responsible for initializing any of the 
        device's required values and returns a URI pointing to the device's UI 
        resource, if applicable. The return value is used to build a launch command 
        that SAVOIR uses to launch the UI application, either in a browser or from 
        the local filesystem.
        
        Throws:
            -OperationFailed if the device connection fails.
            -OperationNotSupported if the device doesn't support the HSVO action type.
        """
        
        # Build the launch URL to send back to SAVOIR when the START message is received.
        return OL_ROOT_URL + parameters.get('sessionID') + '/labyrinth/' + parameters.get('labyrinth')
    
    def start(self):
        """
        start() - This method is responsible for starting the device and any 
        internal mechanisms it may need in order to function properly.
        
        Throws:
            -OperationFailed if the device connection fails.
            -OperationNotSupported if the device doesn't support the HSVO action type.
        """
        
        # The bus interface doesn't need to explicitly start anything; by launching 
        # the previously returned URI the session will be automatically started.
        raise OperationNotSupported('The "start" operation is not supported by OpenLabyrinth.')
    
    def stop(self):
        """
        stop() - This method is responsible for stopping the device.
        
        Throws:
            -OperationFailed if the device connection fails.
            -OperationNotSupported if the device doesn't support the HSVO action type.
        """
        
        # Raise an exception since we don't currently support this operation.
        raise OperationNotSupported('The "stop" operation is not supported by OpenLabyrinth.')
    
    def pause(self, sessionID):
        """
        pause(sessionID, message) - This method is responsible for pausing the 
        device, including any internal mechanisms it may have, such as timers 
        and/or event listeners. 
        
        Throws:
            -OperationFailed if the device connection fails.
            -OperationNotSupported if the device doesn't support the HSVO action type.
        """
        
        # Generate the device's XML using the state template.
        template = self.templateEnvironment.get_template('device_state.xml')
        deviceState = template.render(pauseState=1)
        
        # Update the session using an HTTP POST request to the OL session API.
        try:
            url = OL_ROOT_URL + 'session/update/'
            postData = urllib.urlencode({
                'state': deviceState,
                'sessionid': sessionID})
            
            if BI_DEBUG:
                print 'Post data: "%s".' % (postData,)
                
            usock = urllib2.urlopen(url, postData)
            usock.close()
        
        except urllib2.HTTPError:
            raise OperationFailed('There was an HTTP error when trying to update the session.')
        
        return True
    
    def resume(self, sessionID):
        """
        resume(sessionID) - This method is responsible for resuming the device, 
        including any internal mechanisms it may have, such as timers and/or 
        event listeners. 
        
        Throws:
            -OperationFailed if the device connection fails.
            -OperationNotSupported if the device doesn't support the HSVO action type.
        """
        
        # Generate the device's XML using the state template.
        template = self.templateEnvironment.get_template('device_state.xml')
        deviceState = template.render(pauseState=0, pauseMessage='')
        
        # Update the session using an HTTP POST request to the OL session API.
        try:
            url = OL_ROOT_URL + 'session/update/'
            postData = urllib.urlencode({
                'state': deviceState,
                'sessionid': sessionID})
            
            if BI_DEBUG:
                print 'Post data: "%s".' % (postData,)
            
            usock = urllib2.urlopen(url, postData)
            usock.close()
        
        except urllib2.HTTPError:
            raise OperationFailed('There was an HTTP error when trying to update the session.')
        
        return True
    
    def getStatus(self, sessionID):
        """
        getStatus(sessionID) - This method is responsible for accessing the 
        device's parameters and returning them in this format:
        
        parameters = [{
            'id': parameter.id,
            'name': parameter.name,
            'unit': parameter.unit,
            'value': parameter.value
        },{
            ...
        }]
        
        Throws:
            -OperationFailed if the device connection fails.
            -OperationNotSupported if the device doesn't support the HSVO action type.
        """
        
        # Fetch the pre-existing session using an HTTP POST request to the OL session API.
        try:
            url = OL_ROOT_URL + 'session/read/'
            postData = urllib.urlencode({'sessionid': sessionID})
            usock = urllib2.urlopen(url, postData)
            message = usock.read()
            usock.close()
        
        except urllib2.HTTPError:
            raise OperationFailed('There was an HTTP error when trying to read the session.')
            
        if BI_DEBUG:
            print '\nRetrieved status from device...'
            print('==== Status Message ====================================\n')
            print(message+'\n')
        
        # Send the returned data to a static helper function, which will parse and return 
        # an intermediate data structure. We'll return this to the calling scope so
        # that it can be used to generate the corresponding HSVO "status" message.
        return HSVODeviceInterface.getParameters(message)
    
    def getProfile(self):
        """
        getProfile() - This method is responsible for retrieving the device's profile.
        
        Throws:
            -OperationFailed if the device connection fails.
            -OperationNotSupported if the device doesn't support the HSVO action type.
        """
        
        raise OperationNotSupported('The "getProfile" operation is not supported by OpenLabyrinth.')
    
    def setParameter(self, sessionID, deviceParameters):
        """
        setParameter(sessionID, deviceParameters) - This method is responsible for 
        pushing parameters into the device's internal state. The parameters to be 
        used are in the same format as the expected return value of getStatus().
        
        Throws:
            -OperationFailed if the device connection fails.
            -OperationNotSupported if the device doesn't support the HSVO action type.
        """
        
        # Generate the device's XML using the state template.
        template = self.templateEnvironment.get_template('device_state.xml')
        deviceState = template.render(parameters=deviceParameters)
        
        if BI_DEBUG:
            print "Sending updated state to device:\n %s" % (deviceState,)
        
        # Build and the URL and format the data we're going to send to the device.
        url = OL_ROOT_URL + 'session/update/'
        postData = urllib.urlencode({'state': deviceState, 'sessionid': sessionID})
            
        if BI_DEBUG:
            print 'Post data: \n"%s".' % (postData,)
        
        try:
            usock = urllib2.urlopen(url, postData)
            returnedState = usock.read().strip()
            usock.close()
            
        except urllib2.HTTPError:
            raise OperationFailed("Communication with the device failed: \n %s" % (returnedState,))
        
        return True
    
    def notify(self, sessionID, message):
        """
        notify(sessionID, message) - This method is responsible for responding when the session has ended.
        
        Throws:
            -OperationFailed if the device connection fails.
            -OperationNotSupported if the device doesn't support the HSVO action type.
        """
        
        # Raise an exception since we don't currently support this operation.
        raise OperationNotSupported('The "notify" operation is not supported by OpenLabyrinth.')
    
    def endSession(self, sessionID):
        """
        endSession(sessionID) - This method is responsible for doing any cleanup 
        in the device that may be required, such as purging sessions, finalizing 
        reports or other similar activities.
        
        Throws:
            -OperationFailed if the device connection fails.
            -OperationNotSupported if the device doesn't support the HSVO action type.
        """
        
        # Delete the device's session using its API.
        try:
            url = OL_ROOT_URL + 'session/delete/'
            postData = urllib.urlencode({'sessionid': sessionID})
            
            if BI_DEBUG:
                print 'Post data: "%s".' % (postData,)
            
            usock = urllib2.urlopen(url, postData)
            usock.close()
            
        except urllib2.HTTPError:
            raise OperationFailed("Communication with the device failed: \n %s" % (returnedState,))
        
        return True
    
    def sessionEnded(self):
        """
        sessionEnded() - This method is responsible for responding when the session has ended.
        
        Throws:
            -OperationFailed if the device connection fails.
            -OperationNotSupported if the device doesn't support the HSVO action type.
        """
        
        # Raise an exception since we don't currently support this operation.
        raise OperationNotSupported('The "sessionEnded" operation is not supported by OpenLabyrinth.')
    
    def getParameters(message):
        """
        getParameters(message) - This method parses a device-specific message and 
        produces the data structure to be returned by the getStatus() method. See 
        the getStatus() docstring for the aforementioned data structure's format.
        
        Throws:
            -OperationFailed if the device connection fails.
            -OperationNotSupported if the device doesn't support the HSVO action type.
        """
        
        # Parse the device's message so that we can extract the parameters.
        xmlData = minidom.parseString(message)
        
        # Add some initial device parameters that aren't counter-based.
        parameters = [{
            'id': 'TIME_REMAINING',
            'name': 'Time Remaining',
            'value': xmlData.getElementsByTagName('time')[0].getAttribute('remaining'),
            'unit': 'seconds'
        }, {
            'id': 'CURRENT_NODE',
            'name': 'Current Node',
            'value': xmlData.getElementsByTagName('currentNode')[0].getAttribute('id'),
            'unit': 'null'
        }]
        
        # Add a parameter for every counter in the device's message.
        for counter in xmlData.getElementsByTagName('counter'):
            if counter.getAttribute('unit') == "None":
                # Don't add a unit value and let the template substitute a default.
                parameters.append({
                    'id': counter.getAttribute('id'),
                    'name': counter.getAttribute('name'),
                    'value': counter.getAttribute('value')
                })
            else:
                parameters.append({
                    'id': counter.getAttribute('id'),
                    'name': counter.getAttribute('name'),
                    'value': counter.getAttribute('value'),
                    'unit': counter.getAttribute('unit')
                })
        
        return parameters
    
    def getSessionID(message):
        """
        getSessionID(message) - This method parses a device-specific message and 
        returns the session ID contained within. It's used to get a shared mapping
        list after a message is received in HSVODeviceMessageHandler, so that the 
        appropriate SAVOIR session can be notified of the updated parameters.
        
        Throws:
            -OperationFailed if the device connection fails.
            -OperationNotSupported if the device doesn't support the HSVO action type.
        """
        
        # Parse the device's message so that we can extract the session ID.
        xmlData = minidom.parseString(message)
        
        return xmlData.documentElement.getAttribute('sessionID')
    
    getParameters = staticmethod(getParameters)
    getSessionID = staticmethod(getSessionID)


# Define some exception classes for the device interface.
class OperationFailed(Exception):
    def __init__(self, value):
        self.value = value
    def __str__(self):
        return repr(self.value)

class OperationNotSupported(Exception):
    def __init__(self, value):
        self.value = "Device action unsupported: "+value
    def __str__(self):
        return repr(self.value)